<template>
  <div class="swiper2">
    <section class="carousel" :aria-label="ariaLabel" dir="ltr" tabindex="0">
      <div
        class="carousel__viewport"
        ref="viewportRef"
        @pointerdown="onPointerDown"
        @pointermove="onPointerMove"
        @pointerup="onPointerUp"
        @pointercancel="onPointerUp"
        @pointerleave="onPointerUp"
      >
        <ol class="carousel__track" :style="trackStyle">
          <li
            v-for="(item, idx) in items"
            :key="getKey ? getKey(item, idx) : idx"
            class="carousel__slide"
            :aria-hidden="
              idx < activeIndex || idx > activeIndex + (perView - 1)
            "
            :style="{ width: slideWidthPercent }"
          >
            <slot :item="item" :index="idx" />
          </li>
        </ol>
      </div>
    </section>
  </div>
</template>

<script setup lang="ts">
interface GetKeyFn<T = any> {
  (item: T, index: number): string | number;
}

const props = withDefaults(
  defineProps<{
    items: any[];
    perView?: number;
    step?: number;
    ariaLabel?: string;
    getKey?: GetKeyFn;
  }>(),
  {
    perView: 2,
    step: 1,
    ariaLabel: "Carousel",
  }
);

const emit = defineEmits<{ (e: "change", value: number): void }>();

const activeIndex = ref(0);
const viewportRef = ref<HTMLElement | null>(null);
const slideWidthPx = ref(0);
const slideWidthPercent = computed(() => `${100 / props.perView}%`);

function updateSlideWidth() {
  if (process.client && viewportRef.value) {
    slideWidthPx.value = viewportRef.value.clientWidth / props.perView;
  }
}

onMounted(() => {
  updateSlideWidth();
  if (process.client) {
    window.addEventListener("resize", updateSlideWidth);
  }
});

onBeforeUnmount(() => {
  if (process.client) {
    window.removeEventListener("resize", updateSlideWidth);
  }
});

// drag state
const isDragging = ref(false);
const dragStartX = ref(0);
const dragOffsetPx = ref(0);
const activePointerId = ref<number | null>(null);
const dragStartIndex = ref(0);

const maxIndex = computed(() =>
  Math.max(0, props.items.length - props.perView)
);

const trackStyle = computed(() => {
  const base = activeIndex.value * slideWidthPx.value;
  let offset = isDragging.value ? dragOffsetPx.value : 0;
  // Elastic overscroll only for outward drags: left edge (offset > 0), right edge (offset < 0)
  if (isDragging.value) {
    if (activeIndex.value === 0 && offset > 0) {
      offset *= 0.35; // outward at left edge
    } else if (activeIndex.value === maxIndex.value && offset < 0) {
      offset *= 0.35; // outward at right edge
    }
  }
  return {
    transform: `translateX(${-(base - offset)}px)`,
    transition: isDragging.value ? "none" : "transform 300ms",
    willChange: "transform",
  } as Record<string, string>;
});

function clampIndex(index: number) {
  return Math.min(maxIndex.value, Math.max(0, index));
}

function next() {
  const step = Math.max(1, props.step);
  const newIndex = Math.min(maxIndex.value, activeIndex.value + step);
  if (newIndex !== activeIndex.value) {
    activeIndex.value = newIndex;
    emit("change", activeIndex.value);
  }
}

function prev() {
  const step = Math.max(1, props.step);
  const newIndex = Math.max(0, activeIndex.value - step);
  if (newIndex !== activeIndex.value) {
    activeIndex.value = newIndex;
    emit("change", activeIndex.value);
  }
}

function onPointerDown(e: PointerEvent) {
  if (!viewportRef.value) return;
  isDragging.value = true;
  activePointerId.value = e.pointerId;
  try {
    viewportRef.value.setPointerCapture(e.pointerId);
  } catch {}
  dragStartX.value = e.clientX;
  dragOffsetPx.value = 0;
  dragStartIndex.value = activeIndex.value;
}

function onPointerMove(e: PointerEvent) {
  if (!isDragging.value || activePointerId.value !== e.pointerId) return;
  dragOffsetPx.value = e.clientX - dragStartX.value;
}

function onPointerUp(e: PointerEvent) {
  if (!isDragging.value) return;
  if (viewportRef.value && activePointerId.value !== null) {
    try {
      viewportRef.value.releasePointerCapture(activePointerId.value);
    } catch {}
  }
  const width = slideWidthPx.value || 1;
  const deltaSlides = Math.round(-dragOffsetPx.value / width);
  let targetIndex = clampIndex(dragStartIndex.value + deltaSlides);
  // Bounce back only for outward drags: left edge (dragOffsetPx > 0), right edge (dragOffsetPx < 0)
  if (dragStartIndex.value === 0 && dragOffsetPx.value > 0) {
    targetIndex = 0;
  } else if (
    dragStartIndex.value === maxIndex.value &&
    dragOffsetPx.value < 0
  ) {
    targetIndex = maxIndex.value;
  }
  if (targetIndex !== activeIndex.value) {
    activeIndex.value = targetIndex;
    emit("change", activeIndex.value);
  }
  isDragging.value = false;
  activePointerId.value = null;
  dragOffsetPx.value = 0;
}

defineExpose({ next, prev, activeIndex });
</script>

<style scoped lang="scss">
.swiper2 {
  position: relative;
}

.carousel {
  position: relative;
  text-align: center;
  box-sizing: border-box;
  touch-action: pan-y;
  overscroll-behavior: none;

  * {
    box-sizing: border-box;
  }

  &__viewport {
    overflow: hidden;
  }

  &__track {
    display: flex;
    padding: 0 !important;
    position: relative;
  }

  &__slide {
    scroll-snap-stop: auto;
    flex-shrink: 0;
    margin: 0;
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    transform: translateZ(0);
  }

  .carousel__slide {
    padding: 0.1rem;
  }
}
</style>
